今天我們來說明Polars提供的三種join,分別為「"equi joins"」、「"non-equi joins"」、及「"asof join"」。
本日大綱如下:
codepanda
from datetime import date
import polars as pl
df1 = pl.DataFrame(
{"name": ["Tom", "Lisa", "John"], "has_pet": ["Y", "N", "Y"]}
)
shape: (3, 2)
┌──────┬─────────┐
│ name ┆ has_pet │
│ --- ┆ --- │
│ str ┆ str │
╞══════╪═════════╡
│ Tom ┆ Y │
│ Lisa ┆ N │
│ John ┆ Y │
└──────┴─────────┘
df2 = pl.DataFrame(
{
"name": ["Lisa", "John", "Vincent", "Mary"],
"lucky_number": [25, 36, 7, 2],
}
)
shape: (4, 2)
┌─────────┬──────────────┐
│ name ┆ lucky_number │
│ --- ┆ --- │
│ str ┆ i64 │
╞═════════╪══════════════╡
│ Lisa ┆ 25 │
│ John ┆ 36 │
│ Vincent ┆ 7 │
│ Mary ┆ 2 │
└─────────┴──────────────┘
「"Equi joins"」的意思是根據兩個dataframe中所指定的相同列名進行合併。
在Polars中,我們使用pl.DataFrame.join()來進行「"equi joins"」。例如,藉由設定on=
為針對df1
及df2
的「"name"」列進行合併:
df1.join(df2, on="name")
# equivalent to
# df1.join(df2, on="name", how="inner")
shape: (2, 3)
┌──────┬─────────┬──────────────┐
│ name ┆ has_pet ┆ lucky_number │
│ --- ┆ --- ┆ --- │
│ str ┆ str ┆ i64 │
╞══════╪═════════╪══════════════╡
│ Lisa ┆ N ┆ 25 │
│ John ┆ Y ┆ 36 │
└──────┴─────────┴──────────────┘
可以觀察到合併結果為兩行,這是因為「"Lisa"」跟「"John"」是兩個dataframe的「"name"」列都具有的行。這種合併的方式稱為「"inner"」,是how=
參數的預設值。
how=
參數一共有八種,以下我們再舉「"left"」、「"semi"」及兩個為例子。
當how=
設定為「"left"」時:
df1.join(df2, on="name", how="left")
shape: (3, 3)
┌──────┬─────────┬──────────────┐
│ name ┆ has_pet ┆ lucky_number │
│ --- ┆ --- ┆ --- │
│ str ┆ str ┆ i64 │
╞══════╪═════════╪══════════════╡
│ Tom ┆ Y ┆ null │
│ Lisa ┆ N ┆ 25 │
│ John ┆ Y ┆ 36 │
└──────┴─────────┴──────────────┘
可以觀察到合併結果為三行,且三行都是df1
所擁有的。請留意,因為在df2
中沒有「"Tom"」,所以Polars在「"lucky_number"」列將該值填入null。
當how=
設定為「"semi"」時:
df1.join(df2, on="name", how="semi")
shape: (2, 2)
┌──────┬─────────┐
│ name ┆ has_pet │
│ --- ┆ --- │
│ str ┆ str │
╞══════╪═════════╡
│ Lisa ┆ N │
│ John ┆ Y │
└──────┴─────────┘
可以觀察到合併結果和使用「"inner"」時一樣,皆為兩行,但卻只剩下兩列。這是因為「"semi"」的效果更像是一個過濾器,會依據「"name"」列,找出兩個dataframe共同的行(即「"Lisa"」及「"John"」兩行),但卻不將df2
中的列加入結果之中(即df2
的「"lucky_number"」列)。
最後,我們介紹left_on=
及right_on=
,可以讓我們合併兩個不同列名,而且這些列內的值甚至可以使用expr表示。舉例來說,先建立一個df3
如下:
df3 = df2.select(
pl.col("name").str.to_lowercase().alias("uname"), "lucky_number"
)
shape: (4, 2)
┌─────────┬──────────────┐
│ uname ┆ lucky_number │
│ --- ┆ --- │
│ str ┆ i64 │
╞═════════╪══════════════╡
│ lisa ┆ 25 │
│ john ┆ 36 │
│ vincent ┆ 7 │
│ mary ┆ 2 │
└─────────┴──────────────┘
接下來,將df1
的「"name"」列與以df3
「"uname"」列為基礎所建構之expr合併:
(
df1.join(
df3, left_on="name", right_on=pl.col("uname").str.to_titlecase()
)
)
shape: (2, 4)
┌──────┬─────────┬───────┬──────────────┐
│ name ┆ has_pet ┆ uname ┆ lucky_number │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str ┆ i64 │
╞══════╪═════════╪═══════╪══════════════╡
│ Lisa ┆ N ┆ lisa ┆ 25 │
│ John ┆ Y ┆ john ┆ 36 │
└──────┴─────────┴───────┴──────────────┘
當然您也可以反過來寫,將以df1
「"name"」列為基礎所建構之expr與df3
的「"uname"」列合併:
(
df1.join(
df3, left_on=pl.col("name").str.to_lowercase(), right_on="uname"
)
)
shape: (2, 4)
┌──────┬─────────┬───────┬──────────────┐
│ name ┆ has_pet ┆ uname ┆ lucky_number │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str ┆ i64 │
╞══════╪═════════╪═══════╪══════════════╡
│ Lisa ┆ N ┆ lisa ┆ 25 │
│ John ┆ Y ┆ john ┆ 36 │
└──────┴─────────┴───────┴──────────────┘
兩者結果相同。
「"Non-equi joins"」的意思是根據給定的單或多個以expr建構的判斷條件,來合併兩個dataframe。
在Polars中,我們使用pl.DataFrame.join_where()來進行「"non-equi joins"」。舉例來說,先建立一個df4
如下:
df4 = pl.DataFrame({"name": ["Caroline", "Bob"], "lucky_number2": [3, 40]})
shape: (2, 2)
┌──────────┬───────────────┐
│ name ┆ lucky_number2 │
│ --- ┆ --- │
│ str ┆ i64 │
╞══════════╪═══════════════╡
│ Caroline ┆ 3 │
│ Bob ┆ 40 │
└──────────┴───────────────┘
接下來,將df2
與df4
進行「"non-equi joins"」,合併條件為df2
的「"lucky_number"」列是否小於df4
的「"lucky_number2"」列:
(
df2.join_where(df4, pl.col("lucky_number").lt(pl.col("lucky_number2")))
)
shape: (5, 4)
┌─────────┬──────────────┬────────────┬───────────────┐
│ name ┆ lucky_number ┆ name_right ┆ lucky_number2 │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ str ┆ i64 │
╞═════════╪══════════════╪════════════╪═══════════════╡
│ Mary ┆ 2 ┆ Caroline ┆ 3 │
│ Mary ┆ 2 ┆ Bob ┆ 40 │
│ Vincent ┆ 7 ┆ Bob ┆ 40 │
│ Lisa ┆ 25 ┆ Bob ┆ 40 │
│ John ┆ 36 ┆ Bob ┆ 40 │
└─────────┴──────────────┴────────────┴───────────────┘
其結果有五行,且我們看到了有兩行皆含有「"Mary"」,究竟這是如何計算出來的呢?
原來Polars會將df2
中每一行的「"lucky_number"」列值與df4
每一行的「"lucky_number2"」列值進行比較,總共會進行兩輪共4x2=8次比較,最後會留下結果為True
的行。
第一輪中,由df2
每一行的「"lucky_number"」列值與df4
第一行,即「"Caroline"」的「"lucky_number2"」列值3進行比較。結果,只有df2
「"Mary"」的「"lucky_number"」列值2小於3,所以僅有一行符合條件。
而第二輪比較中,由df2
每一行的「"lucky_number"」列值與df4
第二行,即「"Bob"」的「"lucky_number2"」列值40進行比較。結果,df2
每一行的「"lucky_number"」列值皆小於40,所以共有四行符合條件。
接著,讓我們觀察將df4
與df2
進行「"non-equi joins"」的結果:
(
df4.join_where(df2, pl.col("lucky_number").lt(pl.col("lucky_number2")))
)
shape: (5, 4)
┌──────────┬───────────────┬────────────┬──────────────┐
│ name ┆ lucky_number2 ┆ name_right ┆ lucky_number │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ str ┆ i64 │
╞══════════╪═══════════════╪════════════╪══════════════╡
│ Bob ┆ 40 ┆ John ┆ 36 │
│ Bob ┆ 40 ┆ Lisa ┆ 25 │
│ Bob ┆ 40 ┆ Vincent ┆ 7 │
│ Bob ┆ 40 ┆ Mary ┆ 2 │
│ Caroline ┆ 3 ┆ Mary ┆ 2 │
└──────────┴───────────────┴────────────┴──────────────┘
可以發現其結果是相同的,但是由於這次是以df4
與df2
進行「"non-equi joins"」,所以「"name"」列之值會來自df4
,而「"name_right"」列之值會來自df2
。
最後,如果您不喜歡預設後綴「"_right"」,可以使用suffix=
參數來指定想要的後綴。
「"Asof join"」是一種特殊的合併方式,是基於兩個dataframe中所指定列名的相近程度來合併。
在Polars中,我們使用pl.DataFrame.join_asof()來進行「"asof join"」。
其中提到:
This is similar to a left-join except that we match on nearest key rather than equal keys.
最常作為on=
參數的型別為temporal型別。舉例來說,現在有套件A及套件B各自的發佈日期及版本如下:
package_a = pl.DataFrame(
{
"date": [
date(2025, 5, 14),
date(2025, 6, 16),
date(2025, 7, 29),
date(2025, 8, 16),
date(2025, 9, 2),
],
"version": ["v1.1.2", "v1.2.1", "v1.2.2", "v1.3.1", "v1.4.5"],
}
)
shape: (5, 2)
┌────────────┬─────────┐
│ date ┆ version │
│ --- ┆ --- │
│ date ┆ str │
╞════════════╪═════════╡
│ 2025-05-14 ┆ v1.1.2 │
│ 2025-06-16 ┆ v1.2.1 │
│ 2025-07-29 ┆ v1.2.2 │
│ 2025-08-16 ┆ v1.3.1 │
│ 2025-09-02 ┆ v1.4.5 │
└────────────┴─────────┘
package_b = pl.DataFrame(
{
"date": [date(2025, 6, 1), date(2025, 7, 5), date(2025, 8, 19)],
"version": ["v2.0.1", "v2.0.2", "v2.0.3"],
}
)
shape: (3, 2)
┌────────────┬─────────┐
│ date ┆ version │
│ --- ┆ --- │
│ date ┆ str │
╞════════════╪═════════╡
│ 2025-06-01 ┆ v2.0.1 │
│ 2025-07-05 ┆ v2.0.2 │
│ 2025-08-19 ┆ v2.0.3 │
└────────────┴─────────┘
我們可以使用pl.DataFrame.join_asof()
來找出當套件B發佈時,最新的套件A版本為何:
package_b.join_asof(package_a, on="date", strategy="backward")
shape: (3, 3)
┌────────────┬─────────┬───────────────┐
│ date ┆ version ┆ version_right │
│ --- ┆ --- ┆ --- │
│ date ┆ str ┆ str │
╞════════════╪═════════╪═══════════════╡
│ 2025-06-01 ┆ v2.0.1 ┆ v1.1.2 │
│ 2025-07-05 ┆ v2.0.2 ┆ v1.2.1 │
│ 2025-08-19 ┆ v2.0.3 ┆ v1.3.1 │
└────────────┴─────────┴───────────────┘
如果設定coalesce=False
,則可以同時顯示來自兩個dataframe的日期及版本資訊:
(
package_b.join_asof(
package_a, on="date", strategy="backward", coalesce=False
)
)
shape: (3, 4)
┌────────────┬─────────┬────────────┬───────────────┐
│ date ┆ version ┆ date_right ┆ version_right │
│ --- ┆ --- ┆ --- ┆ --- │
│ date ┆ str ┆ date ┆ str │
╞════════════╪═════════╪════════════╪═══════════════╡
│ 2025-06-01 ┆ v2.0.1 ┆ 2025-05-14 ┆ v1.1.2 │
│ 2025-07-05 ┆ v2.0.2 ┆ 2025-06-16 ┆ v1.2.1 │
│ 2025-08-19 ┆ v2.0.3 ┆ 2025-08-16 ┆ v1.3.1 │
└────────────┴─────────┴────────────┴───────────────┘
另一個常見的應用則為股票的下單與成交,有興趣的朋友可以參考教學文件。
codepanda
Pandas的join主要有pd.DataFrame.merge()與pd.DataFrame.join()兩種。pd.DataFrame.merge()
較為通用,而pd.DataFrame.join()
雖然較為挶限,但可作為想要快速對齊索引的便捷函數。
此外,Pandas的asof join可以參考pd.merge_asof()函數。